Repository navigation
Fix 15 open pnpm issues across hosted, vendored and agent modes - #1007
Conversation
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A hosted scan on a Rush repo repoints common/config/rush/pnpm-lock.yaml (and subspace locks) and reports success, but on pnpm >=11 the next `rush install` either fails (ERR_PNPM_TARBALL_URL_MISMATCH) or, on pnpm 11, silently re-resolves the hosted entries to upstream with exit 0. Root cause: `pnpm_trust` skips the trustLockfile write for Rush locks on purpose (rush runs pnpm in common/temp with a pnpm-workspace.yaml it generates), but it never knew which spliced locks were Rush locks, so the run fell through to the generic manual guidance: `pnpm install --trust-lockfile`, a repo-root `trustLockfile` key and a `--store-dir` reinstall. None of those reach rush's install. `rewrite()` now passes `rush_lock_keys` into `pnpm_trust`. When every spliced pnpm lock is a Rush lock (and not a legacy 5.x/6.0 lock), the `redirect_pnpm_trust_lockfile` warning carries a Rush remedy instead: `pnpm_config_trust_lockfile=true rush install`, plus `"usePnpmFrozenLockfileForRushInstall": true` in common/config/rush/experiments.json on pnpm 11, `rush purge` before the reinstall, and `socket-patch vex` to verify. It also says the pnpm 11 failure can be silent. A run that also spliced a non-Rush pnpm lock keeps the generic text and appends a Rush note. The warning code and file writes are unchanged. docs/ecosystems.md and CLI_CONTRACT.md document the Rush pnpm >=11 remedy. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…919) The hosted pnpm unwind (rollback / remove, and the hosted -> vendored takeover and eject that share it) restored each pinned pnpm-lock.yaml resolution from the version document of the default registry (SOCKET_NPM_REGISTRY, npmjs when unset). restore_pnpm_locks still called the default-registry `fetch_dists`; the project-registry lookup added for #908 (`fetch_dists_on`) was wired only into yarn berry and vlt. The lock-sibling `.npmrc` `registry=` was read, but only to decide whether a `tarball:` is written, never to pick the registry the dist comes from, and `@scope:registry` was not read at all. So for a project resolving against a mirror: - a CDN-style mirror `tarball:` came back as a bare `{integrity}`, because npmjs's URL is conventional, and a cold frozen install 404s; - under lockfile-include-tarball-url, npmjs's dist.tarball replaced the mirror URL pnpm had recorded. Both exited 0. pnpm resolves a name against its `.npmrc` `@scope:registry` when the name is scoped and that key is set, otherwise against `registry`. The new `pnpm_lookup_registry` encodes that rule, and the restore now uses it for two things: as the per-name registry for `fetch_dists_on`, which also gives the existing `upstream_registry_fallback` warning and default-registry fallback when the mirror can't be read, and in the `registry_derives_tarball` decision, so a scoped package's conventional scope-registry URL stays derived. A value that still holds an unexpanded `${VAR}` is read as unset (today's default-registry behaviour) and is never fetched as a literal URL. CLI_CONTRACT.md: the npm-family upstream bullet, the SOCKET_NPM_REGISTRY row and the upstream_registry_fallback row now list pnpm's `.npmrc` `registry` / `@scope:registry` next to berry and vlt. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`scan --mode agent packages/a` in a pnpm workspace scanned 0 packages and exited 0, while `rollback packages/a` selected the member's dependencies. Root cause: the npm crawler keeps one CrawledPackage per name@version (`merge_scan_events`' `seen` set), recorded at the first copy the walk meets. In a pnpm workspace that is the root `node_modules/.pnpm` store entry, never the member's `packages/a/node_modules/<dep>` link; in a yarn classic / npm workspace with a version conflict it is whichever member's nested copy the readdir order reaches first. Scan's PATH filter tested only that one recorded path, so the contract rule "in scope iff ANY installed copy sits under a matching path" was never honored for the other copies. Rollback resolves candidates through `find_all_packages_for_rollback`, which returns every copy, hence the divergence. Fix: keep the cheap first pass over the recorded paths, then resolve the purls that missed to every installed copy with the same enumeration rollback's path targets use (new `find_all_packages_for_rollback_reusing`, which reuses the crawl's npm roots), and admit a purl when any copy matches. A path-scoped run now keeps the npm crawl snapshot so the roots are not rediscovered. The crawler's per-purl dedup is unchanged; apply already patches every copy of a selected purl. Unscoped scans are untouched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#888 made hosted and vendored modes refuse a pnpm project with its own v9 lock and no pnpm-workspace.yaml of its own whenever any ancestor held a pnpm-workspace.yaml (redirect_pnpm_settings_elsewhere, vendor_pnpm_settings_elsewhere). governing_workspace_file took the nearest ancestor file without asking whether its `packages:` globs list the project. pnpm 11.28+ and 12 install a directory the nearest file does not list (an examples/ app, a checkout under an unrelated workspace) standalone: its own lock, and settings read only from its own pnpm-workspace.yaml. So both refusals pointed at remedies that do nothing, and neither mode could patch a project 4.0.0 handled. governing_workspace_file now reads the nearest regular ancestor file (FIFO-safe reader) and returns it only when it lists the project, as probed on pnpm 11.28.5 and 12.10.1: - no `packages:`, a null or an empty list: root-only workspace, so no; - otherwise some pattern matches and no `!` pattern does (pnpm's globber treats every negation as an ignore, wherever it sits). A file that does not parse, or a pattern with braces, classes or extglobs the matcher does not model, still counts as listing the project, so #880/#881 stay refused. With no governing file, hosted creates the project's own pnpm-workspace.yaml with trustLockfile: true and vendored wires its override there, as before #888. The glob matcher used by the package.json workspaces check (#884) moves to utils/workspace_globs.rs so both checks share it. CLI_CONTRACT.md scopes both codes to members listed by the root's `packages:` globs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…854) Vendored pnpm takes over a user's exact-version override of the package it vendors (the pin already forces that version, so redirecting the same key keeps its meaning), but the pre-flight only classified package.json `pnpm.overrides`. On pnpm 10.5+ (always on 11/12) the override lives in pnpm-workspace.yaml `overrides:`. With no package.json override the effective key fell back to our canonical `name@version`, so a bare-key pin (`left-pad: 1.3.0`) in the workspace file and its lock mirror were refused as `vendor_override_conflict`, with a detail claiming the lock "does not match package.json's `left-pad@1.3.0`". The versioned-key pin only worked because it happened to equal our canonical key. - The per-entry Insert / Ours / Takeover / conflict rules are factored into classify_override_entries, shared by classify_pkg_override and a new classify_ws_override (modern locks only; legacy pnpm never reads the workspace file). The effective key comes from whichever file carries the override; package.json and the workspace file pinning under different keys refuses naming both files. - check_lock_override names the file the effective key came from (or says it is the key vendoring would add), and conflict details name pnpm-workspace.yaml when that is where the override lives. - workspace_overrides_govern now counts any workspace override key the lock records, vendored values included. Before, once a workspace-only takeover made the value ours, a re-run (or vendoring another package) saw no user key and wrote a package.json copy that shadows the workspace overrides on pnpm 10 (#360 twin). The #853 CLI fixture that used a bare workspace exact pin as its "refused" trigger now uses a range, and a new CLI test pins the takeover. CLI_CONTRACT.md documents the takeover exception. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…633) In a pnpm (or Bun isolated) workspace, agent-mode `apply` reported every package a member links twice: once `applied`, then a phantom `skipped` / `already_patched` for the same purl, and every re-run counted one extra skip per member-linked package. `rollback` double-counted `alreadyOriginal` the same way. Root cause: the multi-copy resolver (`find_all_packages_for_purls` / `find_all_packages_for_rollback`) runs `find_by_purls` once per `node_modules` root. The workspace root pass records the store entry `node_modules/.pnpm/<pkg>@<ver>/node_modules/<pkg>` and the member pass records the member's `packages/a/node_modules/<pkg>` link to it. `merge_npm_copies` dedupes by literal path, so both spellings of one directory survive and the apply/rollback per-copy loops visited the same physical copy twice. Fix: collapse each npm purl's copies to distinct real directories at the two sites that act per copy (apply's copy loop and rollback's restore targets), keeping the first-found spelling, via a new `distinct_npm_copies` built on the canonical-path helper `distinct_install_dirs` (moved from apply.rs to ecosystem_dispatch.rs, where PyPI apply keeps using it). The resolver map itself still carries every spelling on purpose: path targets (`scan --mode agent packages/a`, `rollback packages/a`, #778) match copies textually through the member's link, and deduping there would make those scopes select nothing. Genuinely distinct copies (nested duplicates, store peer variants, bundled copies) have distinct real paths and are unaffected; a path that can't be canonicalized is kept as-is. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The hosted pnpm unwind (rollback / remove) decides whether a restored pnpm-lock.yaml resolution gets a `tarball:` from PnpmTarballPolicy. That policy read `lockfileIncludeTarballUrl` from pnpm-workspace.yaml, else `lockfile-include-tarball-url` from .npmrc, whatever pnpm major wrote the lock. But pnpm <= 9 ignores pnpm-workspace.yaml settings and pnpm 11/12 ignore pnpm settings in .npmrc, so in those projects the lock has no `tarball:` fields, yet rollback/remove wrote the registry's dist.tarball into every restored resolution: not byte-exact, and each restored package hard-pinned to that registry. The decision now follows what pnpm actually did, strongest signal first: 1. The lock's own unpinned registry resolutions (registry key per the shared pnpm_registry_key rule, integrity, no git/directory fields, tarball neither hosted nor `file:`). With the setting on pnpm writes `tarball:` on every one, so a single bare one proves it off; failing that, a tarball pnpm could have derived proves it on. Unconventional URLs are recorded either way and give no evidence. 2. The settings file the installed pnpm major reads: the major comes from node_modules/.modules.yaml `packageManager` (JSON on pnpm 10+, YAML before), else package.json's corepack `packageManager`, else a pre-9 lockfileVersion or a shrinkwrap.yaml means pnpm <= 8. <= 9 reads .npmrc only, 10 the workspace file then .npmrc, 11+ the workspace file only. 3. With neither (no install record, only pinned entries, e.g. a fresh clone or a Rush lock), pnpm 10's reading, as before. The splice logic and the unconventional-URL branch (#557) are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
pnpm's globber reads `packages:` with dot matching off: on pnpm 12.10.1, `packages: ['**']` leaves `.github/actions/demo` standalone with its own lock, as do `packages/**` for `packages/.x/demo` and `packages/*` for `packages/.hidden`. The shared matcher let `*`, `?` and `**` match those components, so governing_workspace_file still named the root file for them and hosted / vendored refused with redirect_pnpm_settings_elsewhere / vendor_pnpm_settings_elsewhere, pointing at a file pnpm never reads for that directory. lists_as_member now uses glob_matches_no_dot: a wildcard component never matches a path component starting with `.` unless the pattern component itself starts with `.` (`.github/**` still lists it). The npm/yarn `workspaces` caller keeps its current matching. Tests: the probed dot cases in the core membership test and at the refusal level; the refusal test's settings-only root now carries no trust key (`trustLockfile: false` short-circuited the refusal on main too, so it never exercised the membership rule); and the #880 doc comment moves back onto its own test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With Rush subspaces enabled, each subspace keeps its pnpm lock and the repo-state.json that carries its pnpmShrinkwrapHash side by side under common/config/subspaces/<name>/, and there is no common/config/rush/repo-state.json. The hosted engine rewrote the subspace locks but gated redirect_rush_repo_state_stale on the one fixed common path (RUSH_REPO_STATE_REL), so the warning never fired and `rush install` with preventManualShrinkwrapChanges failed on the hash check with no hint. The gate now pairs each rewritten Rush lock with the repo-state.json in its own directory (the common lock still maps to RUSH_REPO_STATE_REL), so a subspace rewrite warns on its subspace's file and a common-only rewrite is not flagged by an unrelated subspace's file. The in-memory host's selector fetches common/config/subspaces/*/repo-state.json presence-only as well, so disk and memory runs agree. Docs and CLI_CONTRACT name the per-subspace file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
pnpm 11+ writes the env lockfile (configDependencies /
packageManagerDependencies) as a first YAML document ahead of the project
lock, and records its resolutions as a bare `{integrity}` even under
lockfileIncludeTarballUrl (verified with pnpm 11.27.0 and
`pnpm add --config is-number@7.0.0`). The tier-1 evidence scan walked every
`packages:` section, so that one bare config dependency proved the setting
off and rollback/remove restored a pinned entry without its `tarball:`,
exiting 0 with a lock that was not byte-exact.
The evidence scan now reads only the main document (after the last `---`
marker), through a new shared grammar::main_document helper.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Hosted mode creates `packages: ['.']` + `trustLockfile: true` for every v9 root lock with no pnpm-workspace.yaml, and vendored mode creates the same scaffold for its `overrides:` mirror. On pnpm 9.0.0-10.4.x that file turns a single-package project into a root-only workspace, where `pnpm add <pkg>` fails with ERR_PNPM_ADDING_TO_ROOT unless given `-w` (10.5.0 is the first release that adds normally). Those releases read neither setting from the file: `trustLockfile` is pnpm >= 11, and workspace `overrides:` are read from 10.5 on. Direction taken: a version-gated scaffold, not `ignoreWorkspaceRootCheck`. When the project has no pnpm-workspace.yaml and every pnpm pin it carries (at least one) names 9.0-10.4, neither mode creates the file. The pins are the installed node_modules/.modules.yaml `packageManager` (YAML on pnpm 9, JSON on 10+) and package.json `packageManager`, `devEngines.packageManager` and `engines.pnpm`, all read with the FIFO-safe regular-file readers. A pin naming a later pnpm, a range reaching past 10.4, an unparseable pin, or no pin keeps the scaffold, so pnpm >= 11 never loses the setting it needs. - Hosted: the new redirect_pnpm_trust_lockfile variant names the pins, says why nothing was written, and gives the pnpm >= 11 recovery (re-run the scan, or `pnpm install --trust-lockfile`). A created scaffold's detail now notes the `pnpm add -w` caveat. - Vendored: package.json `pnpm.overrides` and the lock are wired as before, the workspace mirror is skipped; a later vendor on pnpm >= 10.5 adds it, and revert undoes all three byte-for-byte. The "Commit ..." next step names pnpm-workspace.yaml only when the file exists. - package.json `packageManager` parsing moves to utils/package_manager.rs, shared with the yarn migration check. Tests cover both modes: core unit tests for the pin reader, vendor and revert (including the upgrade path), in-memory and in-process hosted runs including heal-on-rerun after an upgrade and rollback removing the created file, and the real-pnpm e2e legs now expect no file on pnpm 9. CLI_CONTRACT.md, docs/ecosystems.md and docs/testing/pnpm-compatibility.md describe the rule. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Closing (burn-down agent): this draft has no changes. Its only commit is an empty placeholder ("Start pnpm open-issue sweep"), the diff against Generated by Claude Code |
|
[final reviewer] Tanmay Singla (@Tanmay182003) One non-merge commit landed after your approval on Generated by Claude Code |
|
bugbot run Generated by Claude Code |
Resolve conflicts with #1027 ({code, message} JSON errors): keep this PR's pnpm rows in CLI_CONTRACT.md with main's top-level `error.code` wording and main's new rollback `error` row, and keep both import sets in scan/mod.rs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
[final reviewer] Merged
Merge-only, no new behavior. Locally: Tanmay Singla (@Tanmay182003) the re-look request above for Generated by Claude Code |
…ssues # Conflicts: # crates/socket-patch-cli/CLI_CONTRACT.md # crates/socket-patch-core/src/hosted/engine.rs Co-Authored-By: Claude <noreply@anthropic.com>
Main (#1058) made DiskSnapshot::root private behind ProjectView::disk_root and added three RewriteOptions fields; read the root through disk_root() and fill the new fields in the test initializer. Co-Authored-By: Claude <noreply@anthropic.com>
The governing workspace root is reported without Windows' \\?\ prefix (governing_workspace_file strips it), but the test built its expected value from the canonicalized tempdir, which keeps it. The test failed on windows-latest only. Strip the prefix from the expectation the same way. Co-Authored-By: Claude <noreply@anthropic.com>
|
[burn-down agent] Pushed bugbot run Generated by Claude Code |
|
bugbot run Generated by Claude Code |
|
Ready for review (burn-down agent) at
Generated by Claude Code |
|
Withdrawn: right after labeling, Generated by Claude Code |
#1008 moved the pnpm vendor wiring into an NpmLockBackend impl and the vendor run's closing lines into print_vendor_closing. Re-apply this branch's changes on that structure instead of reverting it: - PnpmBackend::preflight warns vendor_config_dependency_unpatched when the lock's env document also lists the package (#466); - PnpmBackend::wire skips the pnpm-workspace.yaml mirror for a project pinned to pnpm 9.0-10.4 with no workspace file (#734), and writes the env-document prefix back ahead of the project lock (#466); - print_vendor_closing passes whether pnpm-workspace.yaml exists to commit_hint (#734); - engine.rs imports and CLI_CONTRACT.md keep both sides' additions. Co-Authored-By: Claude <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit f172f2d. Configure here.
|
[burn-down] Ready for review at Generated by Claude Code |
|
[final reviewer] Tanmay Singla (@Tanmay182003) Two more non-merge commits landed after your approval on
Head Generated by Claude Code |
LLM Description written by Claude Code:claude-opus-5-5
Summary
This PR fixes 15 open pnpm issues across hosted, vendored and agent modes. In most of them a run exited 0 and reported success, but pnpm still installed unpatched bytes, frozen installs broke, or a rollback was not byte-exact. Every fix has a regression test that fails on
main, and each one went through adversarial review rounds.Fixes #1006
Fixes #919
Fixes #902
Fixes #854
Fixes #853
Fixes #830
Fixes #778
Fixes #734
Fixes #714
Fixes #713
Fixes #633
Fixes #556
Fixes #492
Fixes #466
Fixes #435
Issues at a glance
governing_workspace_filetook the nearest ancestorpnpm-workspace.yamlwithout checking that itspackages:globs list the project (regression from #888)packages: ~/empty means root-onlyhosted::governing_root::tests::pnpm_project_outside_the_workspace_globs_is_not_refused,in_process_redirect_pnpm::hosted_scan_from_pnpm_project_outside_workspace_globs_pins_and_nests_trust.npmrc/ workspace mirrorpnpm_lookup_registryfollows the pnpm major's rules (@scope:registry,registry, workspaceregistries/registry:) and is used for both the fetch and the tarball decisionin_process_redirect::pnpm_rollback_reads_the_npmrc_mirror_document_for_an_offpath_tarballlockfileIncludeTarballUrlpolicy read both settings files whatever pnpm wrote the lock, and pnpm 11's env document gave false evidencein_process_redirect::pnpm_rollback_stays_bare_when_pnpm11_ignores_npmrc_include_tarball_url(plus pnpm 9 and pnpm 12 twins)package.jsonoverrides and compared raw YAML textclassify_override_entriesalso coverspnpm-workspace.yamloverrides:; values are compared as YAML reads them (quotes and comments ignored), and revert restores the original text byte for bytevendor::pnpm_lock::tests::ws_bare_key_exact_pin_is_taken_over_and_revert_restores_itscan/get --mode vendored --dry-runpreview never asked the pnpm backend about hosted pinswould_refuse.preflight_refused_purlsmatchesin_process_vendor_pnpm_takeover::scan_vendored_over_hosted_pnpm_catalog_dep_keeps_the_hosted_pin,vendor_flow::preview_tests::preview_refuses_hosted_pnpm_catalog_pinmerge_live_dep_refs) keeps refs that another entry moved, and ref lookup follows the parent's rekey (v9, 5.4, 6.0)in_process_vendor_pnpm_parent_child::remove_parent_keeps_the_vendored_child_wiredfind_all_packages_for_rollback_reusing), the same way rollback doesscan_paths_e2e::paths_scope_selects_pnpm_member_linked_copypackages: ['.']scaffold turns a project into a workspace on pnpm 9.0-10.4 (ERR_PNPM_ADDING_TO_ROOT)in_process_redirect_pnpm::hosted_pnpm_9_through_10_4_project_gets_no_root_only_workspace,hosted_memory_engine::pnpm_9_pin_gets_no_root_only_workspace_in_memoryredirect_rush_repo_state_stalewas gated on the single commonrepo-state.jsonpathrepo-state.jsonbeside it, in both the disk and memory enginesin_process_redirect::rush_subspace_repo_state_stale_warning_fires_for_subspace_repo_statepnpm_trustdidn't know which spliced locks were Rush locks, so it gave the generic remedy, which never reachesrush installredirect_pnpm_trust_lockfileremedy (pnpm_config_trust_lockfile=true rush install,usePnpmFrozenLockfileForRushInstall), re-issued on re-runs over locks that are already redirectedin_process_redirect::rush_pnpm_trust_warning_gives_rush_remedymerge_npm_copiesdeduped by literal path, so the store entry and the member link to it were visited twicedistinct_npm_copies)in_process_npm_multicopy::apply_and_rollback_visit_a_pnpm_workspace_member_link_oncegitBranchLockfile, so the stalepnpm-lock.yamlwas pinnedin_process_redirect_pnpm::hosted_scan_refuses_a_git_branch_lockfile_projectsharedWorkspaceLockfile: falsemember locksmember_locksreads the setting the way pnpm does and expandspackages:. Member locks are pinned, discovered and rolled back. The trust key goes to the root. Ported to the in-memory enginein_process_redirect_pnpm::hosted_scan_pins_every_member_lock_with_shared_workspace_lockfile_false,hosted_memory_paritymember-lock casessplit_project_documentedits only the project document and writes the env document back byte for byte. Unrecognised multi-document locks are refusedvendor::pnpm_lock::tests::two_document_lock_vendors_and_reverts_the_project_document, real-pnpme2e_vendor_pnpm_build::pnpm12_package_manager_two_document_lock_vendors_and_revertsglobal/v<N>is split into one root per install for both--globaland--global-prefixnpm_crawler::tests::global_prefix_finds_every_pnpm_isolated_install_copy,in_process_npm_multicopy::apply_global_prefix_patches_every_pnpm_isolated_global_installNew contract codes
All of these are documented in
CLI_CONTRACT.md. Hosted refusals keep the existing hosted semantics: they are warnings, the status stayssuccesswith exit 0 andredirected: 0, the same asredirect_pnpm_unsupportedand the other existing refusals.Refusals
redirect_pnpm_git_branch_lockfile(hosted, Hosted scan with pnpmgitBranchLockfilepins the stale pnpm-lock.yaml and reports success, while pnpm installs unpatched bytes from pnpm-lock.<branch>.yaml #556):gitBranchLockfileis on and a root or memberpnpm-lock.<branch>.yamlexists. No pnpm lock is touched. A vendored to hosted takeover of such a project is refused with the same code and keeps the vendored wiring.vendor_pnpm_git_branch_lockfile(vendored, Hosted scan with pnpmgitBranchLockfilepins the stale pnpm-lock.yaml and reports success, while pnpm installs unpatched bytes from pnpm-lock.<branch>.yaml #556): the same condition, refused before any write. A hosted to vendored takeover keeps the hosted pin.redirect_pnpm_member_locks_unresolved(hosted, Hosted scan on a pnpm workspace withsharedWorkspaceLockfile: falseignores the per-package pnpm-lock.yaml files and reports success while redirecting nothing #492): the member list can't be resolved, so all pnpm pins are withheld instead of pinning a subset. Causes: nopackages:key, an unmodeled glob, a glob that hits the 4096-directory walk cap, or an unreadable member lock.vendor_pnpm_lock_multi_document(vendored, Vendored pnpm 12 withpackageManagerset: the two-document pnpm-lock.yaml makes vendor refuse, andvendor --revert, rollback and the hosted takeover half-revert the project and break frozen installs #466): a multi-document lock that isn't pnpm's env-plus-project layout. Remedy:--mode hosted. A revert over such a lock fails before touching anything, dry run included.Warnings
upstream_pnpm_tarball_setting_guessed(Hosted pnpm rollback/remove adds registrytarball:URLs the lock never had whenlockfileIncludeTarballUrlsits in a settings file the installed pnpm ignores (workspace file on pnpm 9,.npmrcon pnpm 11/12) #902): the restore had no evidence of which pnpm wrote the lock, so it followed pnpm 10's reading, and pnpm 9 or pnpm >= 11 would read the setting differently. Emitted once per lock. It names the entries, the setting it followed and the pnpm that disagrees, and says how to fix it (pin pnpm, or give both files the same value; for Rush,rush.jsonpnpmVersion).vendor_config_dependency_unpatched(Vendored pnpm 12 withpackageManagerset: the two-document pnpm-lock.yaml makes vendor refuse, andvendor --revert, rollback and the hosted takeover half-revert the project and break frozen installs #466): the vendored package is also a pnpm config dependency. Its env-document copy stays the registry's.pnpm_member_lock_ignored(Hosted scan on a pnpm workspace withsharedWorkspaceLockfile: falseignores the per-package pnpm-lock.yaml files and reports success while redirecting nothing #492, in-memory engine): a member is named inprojectRootswithout its workspace root.redirect_pnpm_trust_lockfilegains new variants. Hosted scan on a Rush repo with pnpm 11/12 reports success, butrush installthen fails with ERR_PNPM_TARBALL_URL_MISMATCH, or (pnpm 11.0.0) silently installs the upstream package #713 adds a Rush remedy. Hosted and vendored modes create apackages: ['.']pnpm-workspace.yaml that turns a single-package project into a workspace, sopnpm add <pkg>fails with ERR_PNPM_ADDING_TO_ROOT on pnpm 9.0–10.4 #734 adds three: one for when the scaffold is skipped on pnpm 9.0-10.4, thepnpm add -wcaveat on a created scaffold, and a delete-it note when a re-scan of a pinned 9.0-10.4 project finds an earlier scaffold.Narrowed
redirect_pnpm_settings_elsewhere/vendor_pnpm_settings_elsewhere(Hosted and vendored pnpm 11/12 refuse a standalone project nested under an unrelated pnpm-workspace.yaml (not in itspackages:globs) as a "workspace member", and the suggested fix doesn't work (regression from #888) #1006) now fire only for projects that the ancestor'spackages:globs actually list. A file that doesn't parse, or a glob syntax we don't model, still counts as listing the project, so Hosted scan from a pnpm 11/12 workspace member with its own lock writes trustLockfile into a nested member pnpm-workspace.yaml that pnpm ignores, so the root install fails with ERR_PNPM_TARBALL_URL_MISMATCH #880 and Vendored scan from a pnpm 11/12 workspace member with its own lock writes the override into a nested member pnpm-workspace.yaml that pnpm ignores, so the root frozen install fails and a plainpnpm installsilently reinstalls the unpatched package #881 stay refused.Maintainer decisions
packages: ['.']pnpm-workspace.yaml that turns a single-package project into a workspace, sopnpm add <pkg>fails with ERR_PNPM_ADDING_TO_ROOT on pnpm 9.0–10.4 #734, no pnpm version evidence: the root-only scaffold is still created, so pnpm >= 11 never losestrustLockfile. The warning names thepnpm add -wcaveat and the remedy: apackageManagerpin naming pnpm 9.0-10.4, with every other pnpm pin also inside that range.tarball:URLs the lock never had whenlockfileIncludeTarballUrlsits in a settings file the installed pnpm ignores (workspace file on pnpm 9,.npmrcon pnpm 11/12) #902, no version evidence (no unpinned registry entry, no.modules.yaml, nopackageManager): the restore keeps pnpm 10's reading and emitsupstream_pnpm_tarball_setting_guessed. Rush locks take the major fromrush.jsonpnpmVersion.Behavior changes worth a close look
packages:globs) as a "workspace member", and the suggested fix doesn't work (regression from #888) #1006, Hosted scan on a pnpm workspace withsharedWorkspaceLockfile: falseignores the per-package pnpm-lock.yaml files and reports success while redirecting nothing #492):utils/pnpm_workspace::lists_memberis now the single membership rule used by bothgoverning_workspace_fileandmember_dirs. As a side effect,governing_workspace_filenow applies pnpm's default ignores, sopackages/testunderpackages/*is no longer a member.cargo_workspace::expand_globnow lets a dot-prefixed pattern segment (packages/.*) match dot directories. This also affects Cargo workspace globs.utils/workspace_globs.rs.sharedWorkspaceLockfile: falseignores the per-package pnpm-lock.yaml files and reports success while redirecting nothing #492): a member lock is demoted into its workspace root only when the root's own files confirm that the root reads that lock. Unconfirmed candidates stay roots of their own and are refused under the trust auto-config instead of being dropped.pnpm-workspace.yaml, member locks and branch locks, but only in directories that hold a package manifest.pnpm-lock.yamlfrom npmjs's version document instead of the project's.npmrcregistry, so a mirror project loses itstarball:URL (cold frozen install 404s) or is moved to npmjs #919) depends on the pnpm major: <= 9 reads.npmrconly; 10 lets a workspaceregistriesmap replace.npmrcwholesale; 11+ or unknown merges the two, with the workspace winning per key. A value with an unexpanded${VAR}is read as unset.tarball:URLs the lock never had whenlockfileIncludeTarballUrlsits in a settings file the installed pnpm ignores (workspace file on pnpm 9,.npmrcon pnpm 11/12) #902) reads only the main lock document, after the last---. A workspacelockfileIncludeTarballUrlis true in any case (True,TRUE), as js-yaml reads it..npmrcneeds a lowercasetrue, as ini/nopt reads it (e866e1a, from a Bugbot finding).packages: ['.']pnpm-workspace.yaml that turns a single-package project into a workspace, sopnpm add <pkg>fails with ERR_PNPM_ADDING_TO_ROOT on pnpm 9.0–10.4 #734): on pnpm 9.0-10.4 the workspaceoverrides:mirror is skipped.package.jsonpnpm.overridesand the lock are wired as before.packageManagerparsing moved toutils/package_manager.rs, shared with the yarn migration check.scan <path>(Agent-modescan packages/<member>finds nothing in a pnpm workspace (exit 0), whilerollback packages/<member>selects the same packages #778): a path-scoped run keeps the npm crawl snapshot and resolves the purls that missed the first pass to every installed copy. In a large monorepo this costs one extra enumeration, and only for scoped runs. Unscoped scans are unchanged.preview_vendor_jsontakes both the patch-server origins (pnpm hosted-to-vendored scan/get dry-run previews success for a refused takeover #853) and main's gem takeover refusals. Our BOM callers now use main'sformats::text::strip_bom.workspacesmatcher brace sets, sequences and character classes inline ingoverning_root.rs. This branch had moved that matcher intoutils/workspace_globs.rs, so the merge moves Fix workspace-member refusal for vlt and brace/class globs (#1071, #942) #1073's grammar and its brace/class test into the shared module and keeps pnpm's no-dot rule there. pnpm'spackages:check still fails closed on brace, class and extglob patterns (counts as listing the project), as before.CLI_CONTRACT.mdkeeps main's vlt/glob wording for the lockfile-elsewhere row and the.socketsymlink row, plus this branch's pnpm settings rows.ProjectViewand one governing-lock table, replacing the step list this branch had added the Hosted scan with pnpmgitBranchLockfilepins the stale pnpm-lock.yaml and reports success, while pnpm installs unpatched bytes from pnpm-lock.<branch>.yaml #556 lone-branch-lock refusal to. The refusal now sits before main's "nothing recognizable" step, andpnpm_lock::git_branch_lock_refusaltakes aProjectView, so the disk, snapshot and in-memory paths all name thegitBranchLockfilesetting.formats/pnpm/mod.rskeeps main'sentry_bundled/Bundledexports and this branch'sversionmodule. The hosted engine's member-lock test helper gains main's newyarn_classic_outeroption.packages: ['.']pnpm-workspace.yaml that turns a single-package project into a workspace, sopnpm add <pkg>fails with ERR_PNPM_ADDING_TO_ROOT on pnpm 9.0–10.4 #734 the scan correctly skips the scaffold, and the bench's expectedrewrittenFilesno longer matched.Testing
main, and each fix went through adversarial review rounds.packageManagerset: the two-document pnpm-lock.yaml makes vendor refuse, andvendor --revert, rollback and the hosted takeover half-revert the project and break frozen installs #466 on pnpm 11.27.0 and 12.8.1 (fresh frozen install plus a byte-exact revert). Hosted and vendored modes create apackages: ['.']pnpm-workspace.yaml that turns a single-package project into a workspace, sopnpm add <pkg>fails with ERR_PNPM_ADDING_TO_ROOT on pnpm 9.0–10.4 #734's e2e legs now expect no scaffold on pnpm 9.hosted_memory_paritychecks that the disk and in-memory engines produce the same result for member locks, the trust config, branch-lock refusals, stale member locks under a shared lock, and Rush subspaces, both straight and through path selection.cargo test --workspaceon an arm Mac has 3 failures. All 3 also fail onorigin/mainon the same machine:e2e_vendor_cargo_buildtestsmode_migration_npm::berry_vendored_then_hosted_takeover_leaves_pure_hostedcargo clippy --workspace --all-features -- -D warningsis clean.cargo test -p socket-patch-core --all-features --libgives 5742 passed. The 4 failures are the read-only-permission tests that root bypasses (copy_tree,vlt_heal,pypi_poetry,pypi_requirementswrite-failure cases).in_process_redirect_pnpm(28),hosted_memory_engine(34),hosted_memory_parity(42) andscan_vendor_e2e(37) pass.in_process_redirectgives 131 passed. Its 3 failures arechmodwrite-failure tests that root bypasses.pnpm_include_tarball_reads_the_settings_file_of_the_pnpm_majorwith workspaceTrue/TRUE/'true'(pnpm 11 and unknown major) failed before the change and passes after it.in_process_redirectpnpm cases (22) pass.cargo clippy --workspace --all-features -- -D warningsis clean, andcargo check --workspace --all-features --testscompiles every test target.cargo test -p socket-patch-core --all-features --lib: 5817 passed, includinga_lone_git_branch_lock_names_the_setting; the same 4 root-bypass failures as above.in_process_redirect_pnpm(28),hosted_memory_parity(42),hosted_memory_engine(34),scan_vendor_e2e(37),in_process_vendor_pnpm_takeover(15) andin_process_vendor_pnpm_parent_child(8) pass.in_process_redirect: 133 passed, with the same 3 rootchmodfailures.Out of scope
scan/getdry-run preview still showswould_vendorfor legacy pnpm 5.4/6.0 hosted pins, which aren't lock-text gated.CLI_CONTRACT.mddocuments this and a test pins it. Yarn hosted pins and non-hosted lock-text refusals are not modelled in the preview either.sharedWorkspaceLockfile: falseignores the per-package pnpm-lock.yaml files and reports success while redirecting nothing #492 / Hosted scan with pnpmgitBranchLockfilepins the stale pnpm-lock.yaml and reports success, while pnpm installs unpatched bytes from pnpm-lock.<branch>.yaml #556: following the existing hosted convention, these refusals don't change the run status. Making them a non-success status would change it for every hosted refusal.pnpm-lock.yamlfrom npmjs's version document instead of the project's.npmrcregistry, so a mirror project loses itstarball:URL (cold frozen install 404s) or is moved to npmjs #919: the user or global~/.npmrcis still not read.Bugbot follow-up (26ce12c)
git_branch_locksskipped members it could not list (Unresolved), so withgitBranchLockfileon and no root branch lock it answered "none" and vendored mode could wire root overrides while a member branch lock was live. It now fails closed and names why the members could not be listed. Regression testgit_branch_locks_fail_closed_on_an_unlisted_member_set: red on be89ccc, green on 26ce12c. Local: core lib 5826 pass (4 pre-existing failures that only fail when tests run as root, same on be89ccc),hosted_memory_parity,in_process_redirect_pnpm,in_process_vendor_pnpm_takeoverall pass, clippy-D warningsclean.sharedWorkspaceLockfile: false) read pnpm-workspace.yaml,.npmrcandpackage.jsonbeside the member lock. pnpm reads them from the workspace root, sotarball:lines and the registry could be wrong. It now reads them from the nearest workspace root listing the member (pnpm_settings_prefix). Testrollback_reads_member_lock_settings_from_the_workspace_root: red on 26ce12c (tarball dropped), green on 017dce1.upstream_registry_fallbackrepeated a registry URL'suser:token@. It is now stripped from both the named registry and the quoted error. Testregistry_fallback_warning_drops_url_credentials: red on 017dce1, green on 2b46121.in_process_redirect_pnpm29/29,hosted_memory_parity42/42, coreredirect::upstream106/106, clippy-D warningsclean.in_process_redirectpasses except 3 write-failure tests that also fail on the unchanged tree here (the sandbox runs as root).🤖 Generated with Claude Code
Note
High Risk
Changes pnpm lockfile rewrite, rollback, and workspace discovery paths that directly control whether patched bytes install and whether restores match upstream—high blast radius across hosted, vendored, and agent flows.
Overview
pnpm hosted, vendored, and agent behavior is tightened so installs, rollbacks, and scans match how real pnpm reads workspace settings, member locks, mirrors, and branch locks—instead of reporting success while leaving unpatched bytes or non–byte-exact restores.
Hosted mode now documents (and the implementation aligns with) per-member locks under
sharedWorkspaceLockfile: false(#492), refusal whengitBranchLockfileand branch locks are present (#556), smartertrustLockfile/ root-onlypnpm-workspace.yamlhandling for pnpm 9.0–10.4 vs ≥11 (#734), Rush-specific trust guidance (#713), and subspacerepo-state.jsonpairing (#714). Upstream restore for pnpm is specified to honor mirror registries (#919), inferlockfileIncludeTarballUrlfrom lock evidence and pnpm major (#902, warningupstream_pnpm_tarball_setting_guessed), and read member-lock settings from the workspace root.Vendored mode contract updates cover two-document pnpm 12 locks (#466), workspace
overrides:mirroring and exact-pin takeover (#854), dry-runwould_refusefor hosted pnpm pins the backend would reject (#853), parent/child lock revert semantics (#830), and matching refusals for branch locks and multi-document locks.Agent mode path-scoped scans are documented to count every installed copy (e.g. pnpm member links into the store) (#778), with deduped apply/rollback visits (#633). Global pnpm 11+ isolated install roots are crawled separately (#435).
The bench npm fixture bumps the recorded
packageManagerto pnpm@11.0.0 so redirect/trust expectations stay consistent with the new scaffold rules.CLI_CONTRACT.mdis expanded throughout to capture these rules, remedies, and warning codes; this diff is primarily that contract sync plus the fixture pin.Reviewed by Cursor Bugbot for commit f172f2d. Configure here.
Generated by Claude Code